// Copyright 2007 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package google.apps.reporting;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.DataOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.net.URLEncoder;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.Properties;
import java.util.TimeZone;

import javax.net.ssl.HttpsURLConnection;

/**
 * This contains the logic for constructing and submitting a report request
 * to the Google Apps reporting service and reading the response.
 *
 * A description of the web service protocol can be found at:
 *
 * http://code.google.com/apis/apps/reporting/google_apps_reporting_api.html
 *
 * <p>
 * Example usage:
 * <pre>
 * ReportRunner runner = new ReportRunner();
 * runner.setAdminEmail("admin@example.com");
 * runner.setAdminPassword("passwd");
 * runner.setDomain("example.com");
 * // Run the latest accounts report to standard output.
 * runner.runReport("accounts", null, System.out);
 * // Run the accounts report for May 15, 2007 and save it to out.txt.
 * runner.runReport("accounts", "2007-05-15", new FileOutputStream("out.txt"));
 * </pre>
 * </p>
 */
public class ReportRunner {

  private static final String EMAIL_ARG = "email";
  private static final String PASSWORD_ARG = "password";
  private static final String DOMAIN_ARG = "domain";
  private static final String REPORT_ARG = "report";
  private static final String DATE_ARG = "date";
  private static final String OUT_FILE_ARG = "out";
  private static final String PAGE_ARG = "page";
  private static final String USAGE = "Usage: java " +
      ReportRunner.class.getName() + " --" + EMAIL_ARG + " <email> --" +
      PASSWORD_ARG +  " <password> [ --" + DOMAIN_ARG +" <domain> ] --" +
      REPORT_ARG + " <report name> [ --" + DATE_ARG + " <YYYY-MM-DD> ] [ --" +
      OUT_FILE_ARG + " <file name> ] [ --" + PAGE_ARG + " <page number>";
  private static final String[] PROPERTY_NAMES = new String[] {EMAIL_ARG,
      PASSWORD_ARG, DOMAIN_ARG, REPORT_ARG, DATE_ARG, OUT_FILE_ARG, PAGE_ARG};
  private static final String[] REQUIRED_PROPERTY_NAMES = new String[] {
      EMAIL_ARG, PASSWORD_ARG, REPORT_ARG};
  private static final String AUTH_URL =
      "https://www.google.com/accounts/ClientLogin";
  private static final String REPORTING_URL =
      "https://www.google.com/hosted/services/v1.0/reports/ReportingData";
  private static final DateFormat DATE_FORMAT =
      new SimpleDateFormat("yyyy-MM-dd");
  private static final TimeZone TIME_ZONE = TimeZone.getTimeZone("PST8PDT");
  private static final int PUBLISH_HOUR_OF_DAY = 12;

  private String adminEmail = null;
  private String adminPassword = null;
  private String domain = null;
  private String token = null;
  private int page = 1;
  private boolean reachedEOR = false;
  private boolean hasUserRequestedSinglePage = false;

  /**
   * Default constructor.
   */
  public ReportRunner() {
  }

  public boolean isHasUserRequestedSinglePage() {
    return hasUserRequestedSinglePage;
  }

  public void setHasUserRequestedSinglePage(boolean bParam) {
    this.hasUserRequestedSinglePage = bParam;
  }
  
  public int getPage() {
    return page;
  }

  public void setPage(int page) {
    this.page = page;
  }

  public void setAdminEmail(String adminEmail) {
    this.adminEmail = adminEmail;
  }

  public String getAdminEmail() {
    return adminEmail;
  }

  /**
   * Get the domain of the admin's email address.
   *
   * This gets the portion of the admin's email address after the final at-sign
   * ('@').  In most cases, this is the same as the domain for reporting.  This
   * is used when no domain is set.
   *
   * @return  admin email address domain if the admin email address has a valid
   *          domain, otherwise return <code>null</code>
   */
  public String getAdminEmailDomain() {
    if (adminEmail != null) {
      int atIndex = adminEmail.lastIndexOf('@');
      if (atIndex >= 0) {
        return adminEmail.substring(atIndex + 1);
      }
    }
    return null;
  }

  public void setAdminPassword(String adminPassword) {
    this.adminPassword = adminPassword;
  }

  public String getAdminPassword() {
    return adminPassword;
  }

  public void setDomain(String domain) {
    this.domain = domain;
  }

  public String getDomain() {
    return domain;
  }

  /**
   * Set the authentication token to be used to access the reporting service.
   *
   * This token is set in the login() method, however if the user of this class
   * has obtained an authentication token through other means it can be set
   * with this method.
   *
   * @param token  authentication service token
   */
  public void setToken(String token) {
    this.token = token;
  }

  /**
   * Get the current authentication token used to access the reporting service.
   *
   * @return  the current authentication token, or null if the token has not
   *          not been set
   */
  public String getToken() {
    return token;
  }

  /**
   * Get an authorization token from username and password for use with the
   * report service.
   *
   * This authorization token is cached in the ReportRunner instance.  If a new
   * token is needed, for example if the token is 24 hours old, then call this
   * method again to get a new token.
   *
   * @throws IOException  if error occurs in the HTTPS connection or if the
   *                      credentials are incorrect
   */
  public void login() throws IOException {
    String postContent = "accountType=HOSTED&Email=" + urlEncode(adminEmail) +
        "&Passwd=" + urlEncode(adminPassword);
    HttpsURLConnection connection = (HttpsURLConnection)
        new URL(AUTH_URL).openConnection();
    connection.setRequestMethod("POST");
    connection.setRequestProperty("Content-Length",
        Integer.toString(postContent.getBytes().length));
    connection.setRequestProperty("Content-Type",
        "application/x-www-form-urlencoded");
    connection.setDoInput(true);
    connection.setDoOutput(true);
    connection.setInstanceFollowRedirects(true);
    DataOutputStream out = new DataOutputStream(connection.getOutputStream());
    out.writeBytes(postContent);
    out.flush();
    out.close();
    BufferedReader reader = new BufferedReader(new InputStreamReader(
        connection.getInputStream()));
    String line = reader.readLine();
    while (line != null) {
      if (line.startsWith("SID=")) {
        reader.close();
        token = line.substring("SID=".length());
        break;
      } else {
        line = reader.readLine();
      }
    }
    reader.close();
  }

  /**
   * Run named report for the specified date.
   *
   * @param  reportName                report name as listed in Reporting API
   *                                   docs
   * @param  dateStr                   date to run the report for.  If the date
   *                                   is <code>null</code>, then the report is
   *                                   run for the latest available report date.
   * @param  out                       output stream to write to.  If the output
   *                                   stream is <code>null</code>, then the
   *                                   report is written to standard output.
   * @throws IOException               if an error occurs in the HTTPS
   *                                   connection or in writing to the output
   *                                   stream
   * @throws ReportException           if an error response is returned from
   *                                   report service
   * @throws IllegalArgumentException  if reportName is <code>null</code> or if
   *                                   the date string is incorrectly formatted
   */
  public void runReport(String reportName, String dateStr, OutputStream out)
      throws IOException, ReportException, IllegalArgumentException  {
    Date date = null;
    // Check dateStr argument for correct format.
    if (dateStr != null) {
      try {
        date = DATE_FORMAT.parse(dateStr);
      } catch (ParseException e) {
        throw new IllegalArgumentException("Date is not in the format " +
            "YYYY-MM-DD");
      }
    }
    runReport(reportName, date, out);
  }

  /**
   * Run named report for the specified date.
   *
   * @param  reportName                report name as listed in Reporting API
   *                                   docs
   * @param  date                      date to run the report for.  If the date
   *                                   is <code>null</code>, then the report is
   *                                   run for the latest available report date.
   * @param  out                       output stream to write to.  If the output
   *                                   stream is <code>null</code>, the report
   *                                   is written to standard output.
   * @throws IOException               if an error occurs in the HTTPS
   *                                   connection or in writing to the output
   *                                   stream
   * @throws ReportException           if an error response returned from the
   *                                   report service
   * @throws IllegalArgumentException  if reportName is <code>null</code>
   */
  public void runReport(String reportName, Date date, OutputStream out)
      throws IOException, ReportException, IllegalArgumentException {
    // Check for non-null reportName argument.
    if (reportName == null) {
      throw new IllegalArgumentException("Report name is null.");
    }
    if (getToken() == null) {
      login();
    }
    ReportRequest request = new ReportRequest();
    request.setToken(getToken());
    request.setPage(getPage());
    if (getDomain() != null) {
      request.setDomain(getDomain());
    } else if (getAdminEmailDomain() != null) {
      request.setDomain(getAdminEmailDomain());
    }
    if (date == null) {
      date = getLatestReportDate();
    }
    request.setReportName(reportName);
    request.setDate(DATE_FORMAT.format(date));
    if (out == null) {
      out = System.out;
    }    
    BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out,
        ReportResponse.CHARSET));

    //Fetch all pages untill End-Of-Report or the specified page. 
    do {
      ReportResponse response = new ReportResponse(getReportData(request));
      if (response.getHeaderLine().equalsIgnoreCase("End-Of-Report")) {
        reachedEOR = true;
      } else {
        writeReport(response, writer);
        writer.flush();
        request.setPage(request.getPage() + 1);            
      }
    } while (!reachedEOR && !hasUserRequestedSinglePage);

    writer.close();
  }

  /**
   * Get latest available report date, based on report service time zone.
   *
   * Reports for the current date are available after 12:00 PST8PDT the
   * following day.  We calculate and return the date of the latest available
   * report based on the current time.
   *
   * @return  latest available report date
   */
  private Date getLatestReportDate() {
    Calendar cal = Calendar.getInstance(TIME_ZONE);
    if (cal.get(Calendar.HOUR_OF_DAY) < PUBLISH_HOUR_OF_DAY) {
      cal.add(Calendar.DATE, -2); // day before yesterday
    } else {
      cal.add(Calendar.DATE, -1); // yesterday
    }
    return cal.getTime();
  }

  /**
   * URL encode a string.
   *
   * @param  s                             string to URL encode
   * @return                               URL encoded string
   * @throws UnsupportedEncodingException  if the character set is not supported
   */
  private String urlEncode(String s) throws UnsupportedEncodingException {
    return URLEncoder.encode(s, ReportResponse.CHARSET);
  }

  /**
   * Get report response as an input stream.
   *
   * @param  request      report request containing request attributes
   * @throws IOException  if an HTTPS connection error occurs
   */
  private InputStream getReportData(ReportRequest request) throws IOException {
    String postContent = request.toXmlString();
    HttpsURLConnection connection = (HttpsURLConnection)
        new URL(REPORTING_URL).openConnection();
    connection.setRequestMethod("POST");
    connection.setRequestProperty("Content-Length",
        Integer.toString(postContent.getBytes().length));
    connection.setDoInput(true);
    connection.setDoOutput(true);
    connection.setInstanceFollowRedirects(true);
    DataOutputStream out = new DataOutputStream(connection.getOutputStream());
    out.writeBytes(postContent);
    out.flush();
    out.close();
    return connection.getInputStream();
  }

  /**
   * Print report response to writer.
   *
   * @param  response         report response reader
   * @param  writer           output writer to write report reponse to
   * @throws IOException      if an error occurs in the HTTPS connection or in
   *                          writing to the output stream
   * @throws ReportException  if an error response is returned from the report
   *                          service
   */
  private void writeReport(ReportResponse response, BufferedWriter writer)
      throws IOException, ReportException {
    String line = response.getNextLine();
    while (line != null) {
      writer.write(line);
      writer.newLine();
      line = response.getNextLine();
    }
  }

  /**
   * Get a map of name-values from command-line arguments.
   *
   * @param  arg                       command-line arguments
   * @return                           properties represented by the command-
   *                                   line arguments
   * @throws IllegalArgumentException  if the command-line arguments are not
   *                                   correct
   */
  private static Properties getProperties(String[] arg)
      throws IllegalArgumentException {
    Properties properties = new Properties();
    for (int i = 0; i < arg.length; i++) {
      boolean found = false;
      for (int j = 0; j < PROPERTY_NAMES.length; j++) {
        if (arg[i].equals("--" + PROPERTY_NAMES[j])) {
          found = true;
          if (i + 1 < arg.length) {
            properties.setProperty(PROPERTY_NAMES[j], arg[i + 1]);
            i++;
            break;
          } else {
            throw new IllegalArgumentException("Missing value for " +
                "command-line parameter " + arg[i]);
          }
        }
      }
      if (!found) {
        throw new IllegalArgumentException("Unrecognized parameter " + arg[i]);
      }
    }
    for (int i = 0; i < REQUIRED_PROPERTY_NAMES.length; i++) {
      if (properties.getProperty(REQUIRED_PROPERTY_NAMES[i]) == null) {
        throw new IllegalArgumentException("Missing value for " +
            "command-line parameter " + REQUIRED_PROPERTY_NAMES[i]);
      }
    }
    return properties;
  }

  /**
   * Request and run a report based on command-line arguments.
   *
   * @param  arg              command-line arguments
   * @throws IOException      when an error occurs in the HTTPS connections
   *                          or if writing to the output stream results in an
   *                          error
   * @throws ReportException  when the report response contains an error
   */
  public static void main(String[] arg) throws IOException, ReportException {
    Properties props = null;                                                        
    try {
      props = getProperties(arg);
    } catch (IllegalArgumentException e) {
      System.err.println(e.getMessage());
      System.err.println(USAGE);
      return;
    }
    ReportRunner runner = new ReportRunner();
    runner.setAdminEmail(props.getProperty(EMAIL_ARG));
    runner.setAdminPassword(props.getProperty(PASSWORD_ARG));
    runner.setDomain(props.getProperty(DOMAIN_ARG));
    OutputStream out = null;
    if (props.getProperty(OUT_FILE_ARG) != null) {
      out = new FileOutputStream(props.getProperty(OUT_FILE_ARG));
    }

    if (props.getProperty(PAGE_ARG) != null) {
      try {
        runner.setPage(Integer.parseInt(props.getProperty(PAGE_ARG)));
      } catch ( NumberFormatException nfe ) {
        System.err.print(nfe.getMessage());
        return;
      }
      runner.setHasUserRequestedSinglePage(true);
    }
    runner.runReport(props.getProperty(REPORT_ARG), props.getProperty(DATE_ARG),
        out);
  }
}
